package eg.backend.manager.impl;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import org.apache.commons.lang.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import cn.muthos.polyforms.dao.Order;
import eg.backend.domain.Dimension;
import eg.backend.domain.Expense;
import eg.backend.domain.GlobalShop;
import eg.backend.domain.GlobalTag;
import eg.backend.domain.Shop;
import eg.backend.domain.Tag;
import eg.backend.domain.fly.AggregatedRecord;
import eg.backend.domain.fly.TagCount;
import eg.backend.manager.ExpenseManager;
import eg.backend.manager.UserManager;
import eg.backend.query.AggregateSO;
import eg.backend.query.DateRange;
import eg.backend.query.PiPaginationProvider;
import eg.backend.query.QueryDefinition;
import eg.backend.repository.DimensionRepository;
import eg.backend.repository.ExpenseRepository;
import eg.backend.repository.GlobalShopRepository;
import eg.backend.repository.GlobalTagRepository;
import eg.backend.repository.ShopRepository;
import eg.backend.repository.TagRepository;
import eg.backend.util.Constants;
import eg.backend.util.Converter;
import eg.backend.util.DateCalculator;
import eg.backend.util.StringUtil;
import eg.backend.util.TimeDimension;

public class ExpenseManagerImpl implements ExpenseManager {
	@Autowired
	private ExpenseRepository expenseRepository;
	@Autowired
	private DimensionRepository dimensionRepository;
	@Autowired
	private TagRepository tagRepository;
	@Autowired
	private ShopRepository shopRepository;
	@Autowired
	private GlobalShopRepository globalShopRepository;
	@Autowired
	private GlobalTagRepository globalTagRepository;
	@Autowired
	private UserManager userManager;	
	@PersistenceContext
	private EntityManager entityManager;

	@Autowired(required = false)
	private PiPaginationProvider paginationProvider;

	public List<Expense> findDailyExpense(Date date) {
		Date today = DateUtils.truncate(date, Calendar.DATE);
		Date tomorrow = DateUtils.addDays(today, 1);
		List<Expense> daily = expenseRepository.findDaily(today, tomorrow, userManager.getCurrentUserId());
		daily.size();
		return daily;
	}

	@SuppressWarnings("deprecation")
	public List<Expense> update(List<Expense> toUpdate, List<Expense> toDelete) {
		List<Expense> result = new ArrayList<Expense>();
		String ownerId = userManager.getCurrentUserId();
		Date now = new Date();
		//update
		for (Expense expense : toUpdate) {
			boolean needSave = true;
			String id = expense.getId();
			if(id != null) {
				try {
					long intId = Long.parseLong(id);
					if(intId < 0)
						expense.setId(null);
				} catch (Exception e) {					
				}
			}
			Expense old = null;	
			if(expense.getId() != null)
				old = expenseRepository.get(id);
			updateTags(expense, old, ownerId);
			
			if(expense.getId() == null) {
				expense.setOwner(ownerId);
				expense.setInputDate(now);
				if(!StringUtil.isEmpty(expense.getShopName())	) {
					Shop newShop = addShop(expense.getShopName(), ownerId);
					if(newShop != null) {
						expense.setShop(newShop.getId());
						expense.setShopName(newShop.getName());
					}
				}
			} else {
				if(old != null)  {
					old.setAmount(expense.getAmount());
					old.setCategory(expense.getCategory());
					old.setName(expense.getName());
					old.setRemark(expense.getRemark());
					old.setDimensions(expense.getDimensions());
					
					Shop newShop = updateShop(expense.getShopName(), old, ownerId);
					if(newShop != null)
						old.setShop(newShop.getId());
					old.setShopName(expense.getShopName());
					expense = old;
					needSave = false;
				}
			}
			
			Date date = expense.getDate();
			//passed-in date has no hour info. for better comparison on aggregate query, add 1 hour 
			if(date.getHours() == 0) {
				date.setHours(1);
				expense.setDate(date);
			}
			if(needSave)
				expense = expenseRepository.save(expense);
			result.add(expense);
		}
		//delete
		for(Expense expense : toDelete) {
			String tags = expense.getTags();
			if(tags != null && tags.length() > 0) {
				String[] tagIds = tags.split(Constants.TAG_SEPARATOR);
				for(String tagId : tagIds) {
					Tag tag = tagRepository.getByNameAndOwner(tagId, ownerId);
					tag.decreaseUsedTimes();
					decreaseGlobalTag(tag.getName());
					//attached entity needn't be explicitly saved
//					tagRepository.save(tag);
				}
			}
			Shop shop = null;
			String shopKey = expenseRepository.get(expense.getId()).getShop();
			if(shopKey != null)
				shop = shopRepository.get(shopKey);
			if(shop != null) {
				shop.decreaseUsedTimes();
				decreaseGlobalShop(shop.getGlobalShop());
			}
			expenseRepository.delete(expense.getId());
		}
		return result;
	}
	
	private Shop updateShop(String newShopName, Expense old, String owner) {
		Shop oldShop = null;
		if(old.getShop() != null)
			oldShop = shopRepository.get(old.getShop());
		if(StringUtil.isEmpty(newShopName)) {
			if(oldShop != null) {
				oldShop.decreaseUsedTimes();
				decreaseGlobalShop(oldShop.getGlobalShop());
			}
			return null;
		} else {
			if(oldShop == null) {
				return addShop(newShopName, owner);
			} else if(!newShopName.equals(globalShopRepository.get(oldShop.getGlobalShop()).getName())) {			
				oldShop.decreaseUsedTimes();
				decreaseGlobalShop(oldShop.getGlobalShop());
				return addShop(newShopName, owner);
			}
		}
		return oldShop;
	}
	
	private void decreaseGlobalShop(String globalShopKey) {
		GlobalShop globalShop = globalShopRepository.get(globalShopKey);
		if(globalShop != null)
			globalShop.decreaseUsedTimes();
	}	
	private void decreaseGlobalTag(String name) {
		GlobalTag globalTag = tagRepository.getGlobalTag(name);
		if(globalTag != null)
			globalTag.decreaseUsedTimes();
	}
	
	@Transactional(propagation = Propagation.REQUIRES_NEW)
	public Shop addShop(String name, String owner) {	
		boolean isGlobalShopAttached = false;
		GlobalShop globalShop = shopRepository.getGlobalShop(name);
		if(globalShop == null) {
			globalShop = new GlobalShop();
			globalShop.setName(name);
		} else {
			isGlobalShopAttached = true;
			globalShop.incrementUsedTimes();
		}
		Shop shop = globalShop == null ? null : shopRepository.getByNameAndOwner(globalShop.getId(), owner);
		boolean isShopAttached = false;
		if(shop == null) {
			shop = new Shop();
			shop.setUsedBy(owner);
			shop.setName(name);
		} else {
			isShopAttached = true;
			shop.incrementUsedTimes();
		}
		if(!isGlobalShopAttached)
			globalShop = globalShopRepository.save(globalShop);
		shop.setGlobalShop(globalShop.getId());
		if(!isShopAttached)
			shop = shopRepository.save(shop);
		return shop;
	}
	
	private void updateTags(Expense expense, Expense old, String owner) {
		List<String> newTagNames = new ArrayList<String>();//used to update tags field of expense
		List<String> toSave = new ArrayList<String>();//to store newly-added tags that need be saved
		
		String tagNames = expense.getTags();
		if (!StringUtil.isEmpty(tagNames)) {
			for (String tagName : tagNames.split(Constants.TAG_SEPARATOR)) {
				if (tagName == null || tagName.length() == 0)
					continue;
				toSave.add(tagName.trim());
			}
		}

		if (old != null && old.getTags() != null) {
			String[] oldTags = old.getTags().split(Constants.TAG_SEPARATOR);
			for (String oldTagName : oldTags) {
				if (oldTagName == null || oldTagName.length() == 0)
					continue;
				if (!toSave.contains(oldTagName)) {
					// this tag has been removed from this expense
					Tag oldTag = tagRepository.getByNameAndOwner(oldTagName,
							owner);
					oldTag.decreaseUsedTimes();
					decreaseGlobalTag(oldTag.getGlobalTag());
				} else {
					newTagNames.add(oldTagName);
				}
				toSave.remove(oldTagName);
			}
		}
		
		//now toSave are all newly-added tags
		for(String name : toSave) {
			Tag tag = tagRepository.getByNameAndOwner(name, owner);
			GlobalTag globalTag = tagRepository.getGlobalTag(name);
			if(globalTag == null) {
				globalTag = new GlobalTag();
				globalTag.setName(name);
				globalTag = globalTagRepository.save(globalTag);
			} else {
				globalTag.incrementUsedTimes();
			}
			if(tag == null) {
				tag = new Tag();
				tag.setGlobalTag(globalTag.getName());
				tag.setName(name);
				tag.setUsedBy(owner);
				tag = tagRepository.save(tag);
			} else {
				tag.incrementUsedTimes();
			}
			newTagNames.add(name);
		}

		String newTags = "";
		for(String tagName : newTagNames) {
			newTags += tagName + Constants.TAG_SEPARATOR;
		}
		if(newTags.length() > 0) {
			newTags = newTags.substring(0, newTags.length() - Constants.TAG_SEPARATOR.length());
		}
		expense.setTags(newTags);
	}
	
	public void deleteSingle(String id) {
		expenseRepository.delete(id);
	}

	public List<Expense> findDetailByQueryDefinition(QueryDefinition queryDefinition) {
		List<Expense> result = new ArrayList<Expense>();
		String user = userManager.getCurrentUserId();
		if(queryDefinition.getUser() == null) {
			queryDefinition.setUser(user);
		}
		
		if(queryDefinition.getTimeDimension() != null) {
			if(!updateDateRange(queryDefinition)) {
				return result;
			}
		}
		
		AggregateSO so = Converter.toAggregateSO(queryDefinition);
		for (Dimension dimension : so.getDimensions()) {
			so.addDimensionIds(dimension.getEncodedKey(), getDescendantIds(dimension));
		}
		result = query(so, true, false);
		return result;
	}
	
	public List<AggregatedRecord> findByQueryDefinition(QueryDefinition queryDefinition) {
		List<AggregatedRecord> result = new ArrayList<AggregatedRecord>();
		String userId = userManager.getCurrentUserId();
		if(queryDefinition.getUser() == null) {
			queryDefinition.setUser(userId);
		}

		//calculate min and max date is required if viewBy time or timeDimension is not null
		if(queryDefinition.getViewBy() == null || (queryDefinition.getTimeDimension() != null)) {
			if(!updateDateRange(queryDefinition)) {
				return result;
			}
		}
		
		AggregateSO parentSo = Converter.toAggregateSO(queryDefinition);
		Dimension viewBy = parentSo.getViewBy();
		//get all descendant dimension ids other than viewBy
		Map<String, List<String>> otherIds = new HashMap<String, List<String>>();
		for(Dimension otherDimension : parentSo.getDimensions()) {
			if(viewBy == null || !otherDimension.getRootDimension().equals(viewBy.getRootDimension())) {
				otherIds.put(otherDimension.getEncodedKey(), getDescendantIds(otherDimension));
			}
		}
		parentSo.setDimensionIds(otherIds);
		
		if(viewBy == null) {
			//时间维度
			return this.get4Time(parentSo, userId, queryDefinition.getTimeDimension());
		}
		
		//时间之外的其他维度
		//拿到viewBy自己
		List<String> viewById = new ArrayList<String>();
		viewById.add(viewBy.getEncodedKey());
		parentSo.addDimensionIds(viewBy.getEncodedKey(), viewById);
		if (!parentSo.excludes("-1")) {
			double unsortedSum = 0.0;
			List<Expense> expenses = this.query(parentSo, true, false);
			for(Expense expense : expenses)
				unsortedSum += expense.getAmount();
			if (isSumShowable(unsortedSum)) {
				result.add(new AggregatedRecord(getUnsortedName(viewBy), -1,
						unsortedSum));
			}
		}
		
		//get children of viewBy
		List<Dimension> children = viewBy.getChildren();//dimensionRepository.findAvailByParent(viewBy.getEncodedKey(), userId);		
		for(Dimension child : children) {
			if(parentSo.excludes(child.getEncodedKey())) 
				continue;
			queryDefinition.updateConstraint(child);
			AggregateSO so = Converter.toAggregateSO(queryDefinition);
			so.setDimensionIds(otherIds);
			so.addDimensionIds(child.getEncodedKey(), getDescendantIds(child));
			double sum = 0.0;//findSum(so);
			List<Expense> expenses = this.query(so, true, false);
			for(Expense expense : expenses)
				sum += expense.getAmount();
			if(isSumShowable(sum)) {
				result.add(new AggregatedRecord(child.getName(), child.getEncodedKey(), sum));
			}
		}

		return result;
	}

	public List<TagCount> findUsedTags() {
		return tagRepository.findUsedTags(userManager.getCurrentUserId());
	}
	
	//自己常用的应当在前面，其次是自己少用的，再次是自己没用过而别人用过的
	public List<String> findShopNames(String query) {
		String user = userManager.getCurrentUserId();
		List<String> shops = new ArrayList<String>();
		List<String> used = shopRepository.findByNameAndOwner(query, user);
		if(used != null)
			shops.addAll(used);
		List<String> existing = shopRepository.findAllGlobalShopNames();//findNotUsedByNameAndOwner(query, user);
		if(existing != null) {
			for(String item : existing) {
				if(!shops.contains(item))
					shops.add(item);
			}
		}
		return shops;
	}

	public List<String> findTagNames(String query) {
		String user = userManager.getCurrentUserId();
		List<String> tags = new ArrayList<String>();
		List<String> used = tagRepository.findByNameAndOwner(query, user);
		if(used != null)
			tags.addAll(used);
		List<String> existing = tagRepository.findAllGlobalTagNames();
		if(existing != null) {
			for(String item : existing) {
				if(!tags.contains(item))
					tags.add(item);
			}
		}
		return tags;
	}

	public List<TagCount> findUsedShops() {
		List<Shop> shops = shopRepository.findUsedShops(userManager.getCurrentUserId());
		List<TagCount> counts = new ArrayList<TagCount>();
		for(Shop shop : shops) {
			GlobalShop gs = globalShopRepository.get(shop.getGlobalShop());
			if(gs != null) {
				counts.add(new TagCount(gs.getName(), shop.getUsedTimes()));
			}
		}
		return counts;
	}
	
	private boolean updateDateRange(QueryDefinition queryDefinition) {
		Date from = queryDefinition.getStartDate();
		String userId = userManager.getCurrentUserId();
		if(from == null) {
			from = expenseRepository.findMinDate(userId);
			queryDefinition.setStartDate(from);
		}
		Date to = queryDefinition.getEndDate();
		if(to == null) {
			to = expenseRepository.findMaxDate(userId);
			if(to != null) {
				to = DateUtils.addDays(to, 1);
				queryDefinition.setEndDate(to);
			}
		}
		if(from == null || to == null) {
			return false;//no record
		}
		return true;
	}
	
	private List<AggregatedRecord> get4Time(AggregateSO parentSo, String userId, TimeDimension timeDimension) {
		List<AggregatedRecord> result = new ArrayList<AggregatedRecord>();
		DateRange[] ranges = DateCalculator.getRanges(parentSo.getFrom(), parentSo.getTo(), timeDimension);
		for(DateRange range : ranges) {
			parentSo.setFrom(range.getFrom());
			parentSo.setTo(range.getTo());
			if(parentSo.excludes(range.getDateId()))
				continue;
			List<Expense> expenses = this.query(parentSo, true, false);
			double sum = 0.0;
			for(Expense expense : expenses)
				sum += expense.getAmount();
			if(isSumShowable(sum)) {
				result.add(new AggregatedRecord(range.getName(), range.getDateId(), sum));
			}
		}
		return result;
	}
	
	private String getUnsortedName(Dimension viewBy) {
		return viewBy.getParent() == null ? "未分类" : viewBy.getName();
	}
	
	private boolean isSumShowable(double sum) {
		return sum > 0;//0.01?
	}

	private Query generateQL(AggregateSO so, /*String category, */String dimensionId) {
		String ql = "";
		ql = "select distinct e from eg.backend.domain.Expense e where e.owner = :user";
//		if(!StringUtil.isEmpty(category))
//			ql += " and e.category = :category";
		boolean hasFrom = false, hasTo = false;
		if (so.getFrom() != null) {
			ql += " and e.date >= :from ";
			hasFrom = true;
		}
		if (so.getTo() != null) {
			ql += " and e.date <= :to ";
			hasTo = true;
		}
		if(!StringUtil.isEmpty(so.getShop())) {
			ql += "and e.shopName = :shopName";
		}

		if(!StringUtil.isEmpty(dimensionId))
			ql += " and e.dimensions.contains(:dimensionId)";
		
		Query query = entityManager.createQuery(ql);
		query.setParameter("user", so.getUserId());
//		if(!StringUtil.isEmpty(category)) 
//			query.setParameter("category", category);
		if(hasFrom) {
			query.setParameter("from", so.getFrom());
		}
		if(hasTo) {
			query.setParameter("to", so.getTo());
		}
		if(!StringUtil.isEmpty(so.getShop())) {
			query.setParameter("shopName", so.getShop());
		}
		if(!StringUtil.isEmpty(dimensionId))
			query.setParameter("dimensionId", dimensionId);
		System.out.println("ql: " + ql);
		return query;
	}
	
	@SuppressWarnings("unchecked")
	private List<Expense> query(AggregateSO so, boolean isDetail, boolean isCategoryUnsorted) {
		List<Expense> result = new ArrayList<Expense>();
		List<String> resultKeys = new ArrayList<String>();
		List<String> dimensionIds = new ArrayList<String>();
		Map<String, List<String>> constraints = so.getConstraints();
		Set<String> keySet = constraints.keySet();
		List<String> keyList = new ArrayList<String>();
		if(keySet != null) {
			for(String key : keySet) {
				keyList.add(key);
			}
			//1. query using the first condition first
			if(keyList.size() > 0) {
				dimensionIds.addAll(constraints.get(keyList.get(0)));
			}
		}
		
		if(dimensionIds.size() == 0)
			dimensionIds.add("");
		//TODO pagination
		for(String dimensionId : dimensionIds) {
			Query query = generateQL(so, /*category, */dimensionId);
			List<Expense> _result = query.getResultList();
			for(Expense e : _result) {
				//avoid duplication
				if(!resultKeys.contains(e.getId())) {
					result.add(e);
					resultKeys.add(e.getId());
				}
			}
		}
		
		List<Expense> toRemove = new ArrayList<Expense>();
		List<List<String>> constraintsList = new ArrayList<List<String>>();
		for(int i = 1; i < keyList.size(); i++) {
			constraintsList.add(constraints.get(keyList.get(i)));
		}
		//2. then remove entities that don't satisfy other constraints
		for(Expense expense : result) {
			if(!isContains(expense.getDimensions(), constraintsList))
				toRemove.add(expense);
		}
		
		result.removeAll(toRemove);
		return result;
	}
	
	private boolean isContains(Set<String> dimensions, List<List<String>> constraints) {
		for(List<String> list : constraints) {
			if(!contains(dimensions, list))
				return false;
		}
		return true;
	}
	//if dimensions has an element that belongs to constraint
	private boolean contains(Set<String> dimensions, List<String> constraint) {
		for(String dimension : dimensions) {
			if(constraint.contains(dimension))
				return true;
		}
		return false;
	}
	
	private Query generateQuery(AggregateSO so, boolean isDetail, boolean isCategoryUnsorted) {
		Map<String, List<String>> constraints = so.getConstraints();
		Set<String> keySet = constraints.keySet();

		String ql = "";
		String target = "distinct e";
		if(!isDetail) {
			ql = "select sum(ex.amount) from eg.backend.domain.Expense ex where ex.id in ( ";
			target = "distinct e.id";
		}
		ql += "select " + target + " from eg.backend.domain.Expense e where e.owner = " + so.getUserId() + " ";
		
		if (!StringUtil.isEmpty(so.getCategoryPosition())) {
			if(isCategoryUnsorted) {
				ql += "and e.category = " + so.getCategory() + " ";
			} else {
				ql += "and e.category in "
					+ "(select d.id from Dimension d where d.rootDimension=1 "
					+ "and d.position like '" + so.getCategoryPosition() + "%'"
					+ " and (d.owner.id in (1, " + so.getUserId()
					+ ") )) ";
			}
		}
		boolean hasFrom = false, hasTo = false;
		if (so.getFrom() != null) {
			ql += "and e.date >= :start ";
			hasFrom = true;
		}
		if (so.getTo() != null) {
			ql += "and e.date <= :end ";
			hasTo = true;
		}
		if(!StringUtil.isEmpty(so.getTag())) {
			ql += "and (e.tags = :tag or e.tags like :tag || ',%' or e.tags like '%,' || :tag || ',%' or e.tags like '%,' || :tag)";
		}
		if(!StringUtil.isEmpty(so.getShop())) {
			ql += "and (e.shop is not null and e.shop.globalShop.name = :shopName)";
		}
		
		if(keySet != null) {
			for(String key : keySet) {
				ql += " and exists (select d.id from Dimension d where d.id in"
						+ " (:dimension" + key + "Ids)"
						+ " and d.id = some elements(e.dimensions)) ";
			}
		}

		if(!isDetail) {
			ql += " )";
		}
		
		Query query = entityManager.createQuery(ql);
		if(hasFrom) {
			query.setParameter("start", so.getFrom());
		}
		if(hasTo) {
			query.setParameter("end", so.getTo());
		}
		if(!StringUtil.isEmpty(so.getTag())) {
			query.setParameter("tag", so.getTag());
		}
		if(!StringUtil.isEmpty(so.getShop())) {
			query.setParameter("shopName", so.getShop());
		}
		if(keySet != null) {
			for(String key : keySet) {
				query.setParameter("dimension" + key + "Ids", constraints.get(key));
			}
		}
		if(isDetail && paginationProvider != null) {
			query.setFirstResult(paginationProvider.getFirstResult());
			query.setMaxResults(paginationProvider.getMaxResults());
			Order[] orders = paginationProvider.getOrders();
			if(orders != null && orders.length > 0) {
				ql += " order by ";
				for(Order order : orders) {
					ql += order.getSortColumn() + " " + order.getSortDirection() + " ";
				}
			}
		}
		return query;
	}
	
	
	@Transactional(propagation = Propagation.REQUIRES_NEW)
	public void addDimension2Expense(String expenseId, String dimension) {
		String todelete = null;
		Dimension parent = dimensionRepository.get(dimension);
		if(parent == null)
			return;
		Expense expense = expenseRepository.get(expenseId);
		String root = parent.getRootDimension() == null ? parent.getEncodedKey() : parent.getRootDimension();
		for (String dim : expense.getDimensions()) {
			if(dim == null) continue;
			Dimension dim1 = dimensionRepository.get(dim);
			if (dim1 != null && dim1.getRootDimension().equals(root)) {
				todelete = dim;
				break;
			}
		}
		if (todelete != null)
			expense.removeDimension(todelete);
		expense.addDimension(dimension);
		expenseRepository.save(expense);
	}
	
	public String getEffectiveParent(Dimension oldChild, List<String> deletedList) {
		String parent = oldChild.getParent();		
		while (isInList(parent, deletedList)) {
			Dimension parentDim = dimensionRepository.get(parent);
			parent = parentDim.getEncodedKey();
		}		
		return parent;
	}
	
	private boolean isInList(String dimensionItem, List<String> deletedList) {
		return deletedList.contains(dimensionItem);
	}
	
	private List<String> getDescendantIds(Dimension ancestor) {
		return dimensionRepository.findDescendantIds(ancestor.getPosition(),
				ancestor.getRootDimension(), userManager.getCurrentUserId());
	}

	public List<String> findChildrenIdsByDimension(String dimension) {
		List<String> ids = new ArrayList<String>();
		for(Expense expense : expenseRepository.findChildrenByDimension(dimension)) {
			ids.add(expense.getId());
		}
		return ids;
	}
	
	
}
